Completed
Push — master ( e70c86...318007 )
by Esaú
01:50
created

hierarchy-helper.js ➔ instanceDefinedOrNull   C

Complexity

Conditions 10
Paths 4

Size

Total Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 10
c 2
b 0
f 0
nc 4
dl 0
loc 22
rs 6.1368
nop 8

How to fix   Complexity    Many Parameters   

Complexity

Complex classes like hierarchy-helper.js ➔ instanceDefinedOrNull often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
// spec/helpers/hierarchy-helper.js
2
"use strict";
3
4
// :: DEPENDENCIES
5
6
const path = require("path");
7
const root = path.dirname(path.dirname(__dirname));
8
9
module.exports = (hierarchy) => {
10
    // check parameters
11
    if (!Array.isArray(hierarchy)) {
12
        throw new Error("hierarchy must be an array");
13
    }
14
15
    // load dependencies
16
    let deps      = hierarchy.map(value => require(path.join(root, value + ".js")));
17
    let klassName = hierarchy.pop();
18
    let Klass     = deps.pop();
19
    const first   = (hierarchy.length === 0);
20
    const third   = (hierarchy.length >= 3);
21
22
    // :: TESTING
23
24
    // test the last class in the hierarchy tree
25
    describe(klassName, () => {
26
27
        // :: INHERITED PROTOTYPE
28
29
        // all inherit from Object
30
        it("should inherit from 'Object'", () => {
31
            expect(new Klass()).toEqual(jasmine.any(Object));
32
        });
33
34
        // check the hierarchy tree
35
        for (let i = 0; i < hierarchy.length; i += 1) {
36
            it("should inherit from '" + hierarchy[i] + "'", () => {
37
                expect(new Klass()).toEqual(jasmine.any(deps[i]));
38
            });
39
        }
40
41
        // check inherited properties
42
        if (!first) {
43
            it("should have a prototype property named 'name'", () => {
44
                expect(Klass.prototype).toHaveString("name");
45
            });
46
47
            it("should have a prototype property named 'message'", () => {
48
                expect(Klass.prototype).toHaveString("message");
49
            });
50
51
            it("should have a prototype property named 'code'", () => {
52
                expect(Klass.prototype).toHaveMember("code");
53
            });
54
        }
55
56
        // check Object methods
57
        it("should have a prototype method named 'toString()'", () => {
58
            expect(Klass.prototype).toHaveMethod("toString");
59
        });
60
61
        // check inherited methods
62
        if (!first) {
63
            it("should have a prototype method named 'native()'", () => {
64
                expect(Klass.prototype).toHaveMethod("native");
65
            });
66
        }
67
68
        // :: EXTENDED PROTOTYPE
69
70
        // check extended properties
71
        if (first) {
72
            it("should have a prototype property named 'name'", () => {
73
                expect(Klass.prototype).toHaveString("name");
74
            });
75
76
            it("should have a prototype property named 'message'", () => {
77
                expect(Klass.prototype).toHaveString("message");
78
            });
79
80
            it("should have a prototype property named 'code'", () => {
81
                expect(Klass.prototype).toHaveMember("code");
82
            });
83
84
            it("should have a prototype method named 'native()'", () => {
85
                expect(Klass.prototype).toHaveMethod("native");
86
            });
87
        }
88
89
        // :: PROTOTYPE VALUES
90
91
        it("should have the 'class' name in the prototype property named 'name'", () => {
92
            expect(Klass.prototype.name).toEqual(klassName);
93
        });
94
95
        it("should have a dummy default value as message", () => {
96
            expect(Klass.prototype.message).toEqual("thrown");
97
        });
98
99
        it("should have a null default value as code", () => {
100
            expect(Klass.prototype.code).toBeNull();
101
        });
102
103
        // use the tests according to the hierarchy level
104
        if (third) {
105
            testThird(Klass);
106
        } else {
107
            test(Klass);
108
        }
109
110
    });
111
112
};
113
114
// Does a loop for each argument of the Klass constructor.
115
// If the iteration is even, the parameter is defined.
116
// If the iteration is odd, the parameter is null.
117
function instanceDefinedOrNull(Klass, name, message, code, fn1, fn2, fn3, third) {
118
    for (let i = 0; i < 2; i += 1) {
119
        const even1   = (i % 2 === 0);
120
        const arg1    = (even1 ? (third ? message : name) : null);
121
        const source1 = new Klass(arg1);
122
        fn1(source1, even1);
123
        for (let j = 0; j < 2; j += 1) {
124
            const even2   = (j % 2 === 0);
125
            const arg2    = (even2 ? (third ? code : message) : null);
126
            const source2 = new Klass(arg1, arg2);
127
            fn2(source2, even1, even2);
128
            if (!third) {
129
                for (let e = 0; e < 2; e += 1) {
130
                    const even3   = (e % 2 === 0);
131
                    const arg3    = (even3 ? code : null);
132
                    const source3 = new Klass(arg1, arg2, arg3);
133
                    fn3(source3, even1, even2, even3);
134
                }
135
            }
136
        }
137
    }
138
}
139
140
// Tests classes that are a third level subclass, meaning that they require 2 arguments (message and code).
141
function testThird(Klass) {
142
143
    // :: CONSTRUCTOR
144
145
    it("should instantiate without parameters", () => {
146
        let arg1, arg2, test;
147
        test = (() => new Klass(arg1, arg2));
148
        for (let i = 0; i < 2; i += 1) {
149
            arg1 = (i % 2 === 0 ? undefined : null);
150
            for (let j = 0; j < 2; j += 1) {
151
                arg2 = (j % 2 === 0 ? undefined : null);
152
                expect(test).not.toThrowError("parameter 'name' must be a 'string'");
153
                expect(test).not.toThrowError("parameter 'message' must be a 'string'");
154
                expect(test).not.toThrowError("parameter 'code' must be a 'number'");
155
            }
156
        }
157
        test = (() => new Klass());
158
        expect(test).not.toThrowError("parameter 'name' must be a 'string'");
159
        expect(test).not.toThrowError("parameter 'message' must be a 'string'");
160
        expect(test).not.toThrowError("parameter 'code' must be a 'number'");
161
    });
162
163
    it("should instantiate with parameters", () => {
164
        let arg1, arg2;
165
        const test1 = (() => new Klass(arg1));
166
        const test2 = (() => new Klass(arg1, arg2));
167
        const args1 = [undefined, null, Klass.prototype.message];
168
        const args2 = [undefined, null, Math.round(Math.random() * 0xFFFFFFFF)];
169
        for (let i = 0; i < args1.length; i += 1) {
170
            arg1 = args1[i];
171
            expect(test1).not.toThrowError("parameter 'name' must be a 'string'");
172
            expect(test1).not.toThrowError("parameter 'message' must be a 'string'");
173
            expect(test1).not.toThrowError("parameter 'code' must be a 'number'");
174
            for (let j = 0; j < args2.length; j += 1) {
175
                arg2 = args2[j];
176
                expect(test2).not.toThrowError("parameter 'name' must be a 'string'");
177
                expect(test2).not.toThrowError("parameter 'message' must be a 'string'");
178
                expect(test2).not.toThrowError("parameter 'code' must be a 'number'");
179
            }
180
        }
181
    });
182
183 View Code Duplication
    it("should throw an Error if 'message' or 'code' are invalid parameters", () => {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
184
        let arg1, arg2, arg3, test21, test22, test11;
0 ignored issues
show
Unused Code introduced by
The variable arg3 seems to be never used. Consider removing it.
Loading history...
185
        const noStr   = [{}, true, false, 42, 3.1416, -42, -3.1416, () => null];
186
        const noNmb   = [{}, true, false, '', "qwerty", () => null];
187
        test22 = (() => new Klass(arg1, arg2));
188
        test21 = (() => new Klass(null, arg2));
189
        test11 = (() => new Klass(arg1));
190
        if (typeof Symbol === "function") {
191
            noStr.push(Symbol("symbol"));
192
            noNmb.push(Symbol("symbol"));
193
        }
194
        for (let i = 0; i < noStr.length; i += 1) {
195
            arg1 = noStr[i];
196
            expect(test11).toThrowError("parameter 'message' must be a 'string'");
197
            for (let j = 0; j < noNmb.length; j += 1) {
198
                arg2 = noNmb[j];
199
                expect(test22).toThrowError("parameter 'message' must be a 'string'");
200
                expect(test21).toThrowError("parameter 'code' must be a 'number'");
201
            }
202
        }
203
    });
204
205
    // :: MEMBER PROPERTIES
206
207
    const message = "asdf";
208
    const code    = Math.round(Math.random() * 0xFFFFFFFF);
209
210
    it("should have all correct properties once instantiated", () => {
211
        instanceDefinedOrNull(Klass, null, message, code, (instance, even) => {
212
            if (even) {
213
                expect(instance.name).toEqual(Klass.prototype.name);
214
                expect(instance.message).toEqual(message);
215
            } else {
216
                expect(instance.name).toEqual(Klass.prototype.name);
217
                expect(instance.message).toEqual(Klass.prototype.message);
218
            }
219
            expect(instance.code).toBeNull();
220
        }, (instance, even1, even2) => {
221
            expect(instance.name).toEqual(Klass.prototype.name);
222
            expect(instance.message).toEqual(even1 ? message : Klass.prototype.message);
223
            expect(instance.code).toEqual(even2 ? code : null);
224
        }, (instance, even1, even2, even3) => {
225
            expect(instance.name).toEqual(even1 ? name : Klass.prototype.name);
0 ignored issues
show
Bug introduced by
The variable name seems to be never declared. If this is a global, consider adding a /** global: name */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
226
            expect(instance.message).toEqual(even2 ? message : Klass.prototype.message);
227
            expect(instance.code).toEqual(even3 ? code : null);
228
        }, true);
229
    });
230
231
    // :: MEMBER METHODS
232
233
    it("#toString()", () => {
234 View Code Duplication
        for (let i = 0; i < 2; i += 1) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
235
            let exp1;
236
            const even1   = (i % 2 === 0);
237
            const arg1    = (even1 ? message : null);
238
            const source1 = new Klass(arg1);
239
            if (even1) {
240
                exp1 = Klass.prototype.name + ": " + message + '.';
241
            } else {
242
                exp1 = Klass.prototype.name + ": " + Klass.prototype.message + '.';
243
            }
244
            expect(source1.toString()).toEqual(exp1);
245
            for (let j = 0; j < 2; j += 1) {
246
                let exp2;
247
                const even2   = (j % 2 === 0);
248
                const arg2    = (even2 ? code : null);
249
                const source2 = new Klass(arg1, arg2);
250
                exp2 = Klass.prototype.name;
251
                exp2 += (even2 ? " (0x" + code.toString(16) + "):" : ':' ) + ' ';
252
                exp2 += (even1 ? message : Klass.prototype.message) + '.';
253
                expect(source2.toString()).toEqual(exp2);
254
            }
255
        }
256
    });
257
258
    it("#native()", () => {
259
        instanceDefinedOrNull(Klass, null, message, code, (instance, even) => {
260
            const exp = (even ? message : Klass.prototype.message);
261
            expect(instance.native()).toEqual(new Error(exp));
262
        }, (instance, even1) => {
263
            const exp = (even1 ? message : Klass.prototype.message);
264
            expect(instance.native()).toEqual(new Error(exp));
265
        }, (instance, even1, even2) => {
266
            const exp = (even2 ? message : Klass.prototype.message);
267
            expect(instance.native()).toEqual(new Error(exp));
268
        }, true);
269
    });
270
271
}
272
273
// Tests classes that require 3 arguments (name, message and code).
274
function test(Klass) {
275
276
    // :: CONSTRUCTOR
277
278
    it("should instantiate without parameters", () => {
279
        let arg1, arg2, arg3, test;
280
        test = (() => new Klass(arg1, arg2, arg3));
281
        for (let i = 0; i < 2; i += 1) {
282
            arg1 = (i % 2 === 0 ? undefined : null);
283
            for (let j = 0; j < 2; j += 1) {
284
                arg2 = (j % 2 === 0 ? undefined : null);
285
                for (let e = 0; e < 2; e += 1) {
286
                    arg3 = (e % 2 === 0 ? undefined : null);
287
                    expect(test).not.toThrowError("parameter 'name' must be a 'string'");
288
                    expect(test).not.toThrowError("parameter 'message' must be a 'string'");
289
                    expect(test).not.toThrowError("parameter 'code' must be a 'number'");
290
                }
291
            }
292
        }
293
        test = (() => new Klass());
0 ignored issues
show
Comprehensibility introduced by
It seems like you are trying to overwrite a function name here. test is already defined in line 274 as a function. While this will work, it can be very confusing.
Loading history...
294
        expect(test).not.toThrowError("parameter 'name' must be a 'string'");
295
        expect(test).not.toThrowError("parameter 'message' must be a 'string'");
296
        expect(test).not.toThrowError("parameter 'code' must be a 'number'");
297
    });
298
299
    it("should instantiate with parameters", () => {
300
        let arg1, arg2, arg3, test1, test2, test3, args1, args2, args3;
301
        test1 = (() => new Klass(arg1));
302
        test2 = (() => new Klass(arg1, arg2));
303
        test3 = (() => null);
0 ignored issues
show
Unused Code introduced by
The assignment to variable test3 seems to be never used. Consider removing it.
Loading history...
304
        test3 = (() => new Klass(arg1, arg2, arg3));
305
        args1 = [undefined, null, Klass.prototype.name];
306
        args2 = [undefined, null, Klass.prototype.message];
307
        args3 = [undefined, null, Math.round(Math.random() * 0xFFFFFFFF)];
308
        for (let i = 0; i < args1.length; i += 1) {
309
            arg1 = args1[i];
310
            expect(test1).not.toThrowError("parameter 'name' must be a 'string'");
311
            expect(test1).not.toThrowError("parameter 'message' must be a 'string'");
312
            expect(test1).not.toThrowError("parameter 'code' must be a 'number'");
313
            for (let j = 0; j < args2.length; j += 1) {
314
                arg2 = args2[j];
315
                expect(test2).not.toThrowError("parameter 'name' must be a 'string'");
316
                expect(test2).not.toThrowError("parameter 'message' must be a 'string'");
317
                expect(test2).not.toThrowError("parameter 'code' must be a 'number'");
318
                for (let e = 0; e < args3.length; e += 1) {
319
                    arg3 = args3[e];
320
                    expect(test3).not.toThrowError("parameter 'name' must be a 'string'");
321
                    expect(test3).not.toThrowError("parameter 'message' must be a 'string'");
322
                    expect(test3).not.toThrowError("parameter 'code' must be a 'number'");
323
                }
324
            }
325
        }
326
    });
327
328 View Code Duplication
    it("should throw an Error if 'message' or 'code' are invalid parameters", () => {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
329
        let arg1, arg2, arg3, test31, test32, test33, test21, test22, test11;
330
        const noStr   = [{}, true, false, 42, 3.1416, -42, -3.1416, () => null];
331
        const noNmb   = [{}, true, false, '', "qwerty", () => null];
332
        test33 = (() => new Klass(arg1, arg2, arg3));
333
        test32 = (() => new Klass(null, arg2, arg3));
334
        test31 = (() => new Klass(null, null, arg3));
335
        test22 = (() => new Klass(arg1, arg2));
336
        test21 = (() => new Klass(null, arg2));
337
        test11 = (() => new Klass(arg1));
338
        if (typeof Symbol === "function") {
339
            noStr.push(Symbol("symbol"));
340
            noNmb.push(Symbol("symbol"));
341
        }
342
        for (let i = 0; i < noStr.length; i += 1) {
343
            arg1 = noStr[i];
344
            expect(test11).toThrowError("parameter 'name' must be a 'string'");
345
            for (let j = 0; j < noStr.length; j += 1) {
346
                arg2 = noStr[j];
347
                expect(test22).toThrowError("parameter 'name' must be a 'string'");
348
                expect(test21).toThrowError("parameter 'message' must be a 'string'");
349
                for (let e = 0; e < noNmb.length; e += 1) {
350
                    arg3 = noNmb[e];
351
                    expect(test33).toThrowError("parameter 'name' must be a 'string'");
352
                    expect(test32).toThrowError("parameter 'message' must be a 'string'");
353
                    expect(test31).toThrowError("parameter 'code' must be a 'number'");
354
                }
355
            }
356
        }
357
    });
358
359
    // :: MEMBER PROPERTIES
360
361
    const name    = "qwerty";
362
    const message = "asdf";
363
    const code    = Math.round(Math.random() * 0xFFFFFFFF);
364
365
    it("should have all correct properties once instantiated", () => {
366
        instanceDefinedOrNull(Klass, name, message, code, (instance, even) => {
367
            if (even) {
368
                expect(instance.name).toEqual(name);
369
                expect(instance.message).toEqual(Klass.prototype.message);
370
            } else {
371
                expect(instance.name).toEqual(Klass.prototype.name);
372
                expect(instance.message).toEqual(Klass.prototype.message);
373
            }
374
            expect(instance.code).toBeNull();
375
        }, (instance, even1, even2) => {
376
            expect(instance.name).toEqual(even1 ? name : Klass.prototype.name);
377
            expect(instance.message).toEqual(even2 ? message : Klass.prototype.message);
378
            expect(instance.code).toBeNull();
379
        }, (instance, even1, even2, even3) => {
380
            expect(instance.name).toEqual(even1 ? name : Klass.prototype.name);
381
            expect(instance.message).toEqual(even2 ? message : Klass.prototype.message);
382
            expect(instance.code).toEqual(even3 ? code : null);
383
        }, false);
384
    });
385
386
    // :: MEMBER METHODS
387
388
    it("#toString()", () => {
389
        for (let i = 0; i < 2; i += 1) {
390
            let exp1;
391
            const even1   = (i % 2 === 0);
392
            const arg1    = (even1 ? name : null);
393
            const source1 = new Klass(arg1);
394
            if (even1) {
395
                exp1 = name + ": " + Klass.prototype.message + '.';
396
            } else {
397
                exp1 = Klass.prototype.name + ": " + Klass.prototype.message + '.';
398
            }
399
            expect(source1.toString()).toEqual(exp1);
400 View Code Duplication
            for (let j = 0; j < 2; j += 1) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
401
                let exp2;
402
                const even2   = (j % 2 === 0);
403
                const arg2    = (even2 ? message : null);
404
                const source2 = new Klass(arg1, arg2);
405
                exp2 = (even1 ? name : Klass.prototype.name) + ':';
406
                exp2 += ' ' + (even2 ? message : Klass.prototype.message) + '.';
407
                expect(source2.toString()).toEqual(exp2);
408
                for (let e = 0; e < 2; e += 1) {
409
                    let exp3;
410
                    const even3   = (e % 2 === 0);
411
                    const arg3    = (even3 ? code : null);
412
                    const source3 = new Klass(arg1, arg2, arg3);
413
                    exp3  = (even1 ? name : Klass.prototype.name);
414
                    exp3 += (even3 ? " (0x" + code.toString(16) + "):" : ':') + ' ';
415
                    exp3 += (even2 ? message : Klass.prototype.message) + '.';
416
                    expect(source3.toString()).toEqual(exp3);
417
                }
418
            }
419
        }
420
    });
421
422
    it("#native()", () => {
423
        instanceDefinedOrNull(Klass, name, message, code, (instance) => {
424
            const exp = Klass.prototype.message;
425
            expect(instance.native()).toEqual(new Error(exp));
426
        }, (instance, even1, even2) => {
427
            const exp  = (even2 ? message : Klass.prototype.message);
428
            expect(instance.native()).toEqual(new Error(exp));
429
        }, (instance, even1, even2) => {
430
            const exp = (even2 ? message : Klass.prototype.message);
431
            expect(instance.native()).toEqual(new Error(exp));
432
        }, false);
433
    });
434
435
}